﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using StyleMVVM.Validation.Impl.Fluent.Operators;

namespace StyleMVVM.Validation.Impl.Fluent
{
	public class FluentPropertyExpressionProvider<T> : IFluentExpressionProvider
	{
		private readonly Dictionary<string, ParameterExpression> properties = new Dictionary<string, ParameterExpression>();
		private readonly FluentPropertyExpressionProvider<T> parent;

		public FluentPropertyExpressionProvider()
		{
		}

		public FluentPropertyExpressionProvider(FluentPropertyExpressionProvider<T> parent)
		{
			this.parent = parent;
		}

		public ParameterExpression GetPropertyExpression(string propertyName)
		{
			ParameterExpression returnValue = null;

			if (parent != null && parent.properties.TryGetValue(propertyName, out returnValue))
			{
				return returnValue;
			}

			if (properties.TryGetValue(propertyName, out returnValue))
			{
				return returnValue;
			}

			returnValue = Expression.Variable(typeof(object), "p" + propertyName.Replace('.', '_'));

			properties.Add(propertyName, returnValue);

			return returnValue;
		}

		public ParameterExpression ValidationObject { get; set; }

		public ParameterExpression ExecutionContext { get; set; }

		public IEnumerable<Expression> PropertyExpressions(out IEnumerable<ParameterExpression> variables)
		{
			List<Expression> propertyFetchStatements = new List<Expression>();

			List<ParameterExpression> tempVariables = new List<ParameterExpression>();

			foreach (KeyValuePair<string, ParameterExpression> propertyKVP in properties)
			{
				Expression fetchStatement =
					CreatePropertyFetchCode(ValidationObject, typeof(T), propertyKVP.Key, propertyKVP.Value);

				propertyFetchStatements.Add(fetchStatement);

				tempVariables.Add(propertyKVP.Value);
			}

			variables = tempVariables;

			return propertyFetchStatements;
		}

		private static Expression CreatePropertyFetchCode(ParameterExpression valueParameter,
		                                                  Type objectType,
		                                                  string propertyName,
		                                                  ParameterExpression variable)
		{
			Expression returnExpression = null;
			int firstPeriod = propertyName.IndexOf('.');

			if (firstPeriod > 0)
			{
				string currentPropertyName = propertyName.Substring(0, firstPeriod);
				string theRest = propertyName.Substring(firstPeriod + 1);
				Type propertyOrFieldType = CreateGetPropertyOrFieldType(objectType, currentPropertyName);

				ParameterExpression newValue = Expression.Variable(propertyOrFieldType);

				Expression assignExpression = Expression.Assign(newValue,
				                                                Expression.PropertyOrField(valueParameter, currentPropertyName));

				if (!propertyOrFieldType.GetTypeInfo().IsValueType)
				{
					Expression recurse =
						CreatePropertyFetchCode(newValue,
						                        propertyOrFieldType,
						                        theRest,
						                        variable);

					Expression ifExpression = Expression.IfThen(
						Expression.NotEqual(newValue, Expression.Constant(null)),
						recurse);

					returnExpression =
						Expression.Block(new[] { newValue }, new[] { assignExpression, ifExpression });
				}
				else
				{
					Expression recurse = CreatePropertyFetchCode(newValue,
					                                             propertyOrFieldType,
					                                             theRest,
					                                             variable);

					returnExpression =
						Expression.Block(new[] { newValue }, new[] { assignExpression, recurse });
				}
			}
			else
			{
				returnExpression = Expression.Assign(variable,
				                                     Expression.TypeAs(Expression.PropertyOrField(valueParameter, propertyName),
				                                                       typeof(object)));
			}

			return returnExpression;
		}

		private static Type CreateGetPropertyOrFieldType(Type objectType, string propertyName)
		{
			Type propertyOrFieldType = null;

			PropertyInfo propertyInfo =
				objectType.GetTypeInfo().GetDeclaredProperty(propertyName);

			if (propertyInfo == null)
			{
				FieldInfo field = objectType.GetTypeInfo().GetDeclaredField(propertyName);

				if (field != null)
				{
					propertyOrFieldType = field.FieldType;
				}
				else
				{
					throw new Exception(
						string.Format("Could not find property {0} on type {1}",
						              objectType.FullName,
						              propertyName));
				}
			}
			else
			{
				propertyOrFieldType = propertyInfo.PropertyType;
			}

			return propertyOrFieldType;
		}
	}
}